package io.hellsinger.vortex.component.gesture

import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
import android.widget.OverScroller
import io.hellsinger.vortex.ui.component.StorageTextEditorView
import kotlin.math.abs

internal class MotionTracker internal constructor(
    private val target: StorageTextEditorView,
) {
    private var velocity = VelocityTracker.obtain()
    private var scroller = OverScroller(target.context)

    private var activePointerId = NO_POINTER

    private val configuration = ViewConfiguration.get(target.context)

    private val touchSlop = configuration.scaledTouchSlop
    private val maxFlingVelocity = configuration.scaledMaximumFlingVelocity
    private val minFlingVelocity = configuration.scaledMinimumFlingVelocity
    private val overScrollDistance = configuration.scaledOverscrollDistance

    private var state: Int = SCROLL_STATE_IDLE

    private var lastMotionX = 0F
    private var lastMotionY = 0F

    val horizontalScrollOffset: Int
        get() = scroller.currX

    val verticalScrollOffset: Int
        get() = scroller.currY

    val scrollVelocity: Float
        get() = scroller.currVelocity

    fun onMotion(event: MotionEvent): Boolean {
        val action = event.action
        velocity.addMovement(event)
        return when (action) {
            MotionEvent.ACTION_DOWN -> {
                // Lets abort our scroll without instantly setting scroll offsets
                scroller.forceFinished(true)

                lastMotionX = event.x
                lastMotionY = event.y
                activePointerId = event.getPointerId(PRIMARY_POINTER_INDEX)
                true
            }

            MotionEvent.ACTION_MOVE -> {
                val pointerIndex = event.findPointerIndex(activePointerId)
                val oldHorizontalScrollOffset = horizontalScrollOffset
                val oldVerticalScrollOffset = verticalScrollOffset
                val x = event.getX(pointerIndex)
                val y = event.getY(pointerIndex)
                val horizontalScrollDistance =
                    clampIfCantScrollHorizontally(
                        value = (lastMotionX - x).toInt(),
                        default = 0,
                    )
                val verticalScrollDistance =
                    clampIfCantScrollVertically(
                        value = (lastMotionY - y).toInt(),
                        default = 0,
                    )

                if (state != SCROLL_STATE_DRAGGING) {
                    if (isScrollAction(horizontalScrollDistance, verticalScrollDistance)) {
                        state = SCROLL_STATE_DRAGGING

//                        Let distances consume touch slop that we ignored previously
//                        verticalScrollDistance += if (verticalScrollDistance > 0) -touchSlop else touchSlop
//                        horizontalScrollDistance += if (horizontalScrollDistance > 0) -touchSlop else touchSlop
                    }
                }
                if (state == SCROLL_STATE_DRAGGING) {
                    scroll(
                        toHorizontalScrollOffset =
                            clampHorizontalScrollOffset(
                                horizontalScrollOffset + horizontalScrollDistance,
                            ) - horizontalScrollOffset,
                        toVerticalScrollOffset =
                            clampVerticalScrollOffset(
                                verticalScrollOffset + verticalScrollDistance,
                            ) - verticalScrollOffset,
                    )

                    // Vertical overscroll
                    target.onPullScrollOffset(
                        oldX = oldHorizontalScrollOffset,
                        oldY = oldVerticalScrollOffset,
                        horizontalScrollDistance = horizontalScrollDistance.toFloat(),
                        verticalScrollDistance = verticalScrollDistance.toFloat(),
                        horizontalDisplacement = x / target.width,
                        verticalDisplacement = y / target.height,
                    )
                }
                lastMotionX = x
                lastMotionY = y
                true
            }

            MotionEvent.ACTION_CANCEL -> {
                clear()
                state = SCROLL_STATE_IDLE
                activePointerId = NO_POINTER
                true
            }

            MotionEvent.ACTION_UP -> {
                if (state == SCROLL_STATE_DRAGGING) {
                    velocity.computeCurrentVelocity(
                        // units =
                        VELOCITY_UNITS_PER_SECOND,
                        // maxVelocity =
                        maxFlingVelocity.toFloat(),
                    )
                    val horizontalFlingVelocity =
                        clampIfCantScrollHorizontally(
                            value = velocity.getXVelocity(activePointerId).toInt(),
                            default = 0,
                        )
                    val verticalFlingVelocity =
                        clampIfCantScrollVertically(
                            value = velocity.getYVelocity(activePointerId).toInt(),
                            default = 0,
                        )

                    if (abs(verticalFlingVelocity) > minFlingVelocity || abs(horizontalFlingVelocity) > minFlingVelocity) {
                        fling(
                            horizontalVelocity = -horizontalFlingVelocity,
                            verticalVelocity = -verticalFlingVelocity,
                        )
                    }
                } else {
                    target.performClick()
                }
                clear()
                state = SCROLL_STATE_IDLE
                activePointerId = NO_POINTER
                true
            }

            else -> false
        }
    }

    fun computeScrollOffset() = scroller.computeScrollOffset()

    private fun scroll(
        fromHorizontalScrollOffset: Int = horizontalScrollOffset,
        fromVerticalScrollOffset: Int = verticalScrollOffset,
        toHorizontalScrollOffset: Int,
        toVerticalScrollOffset: Int,
        duration: Int = 0,
    ) {
        scroller.startScroll(
            fromHorizontalScrollOffset,
            fromVerticalScrollOffset,
            toHorizontalScrollOffset,
            toVerticalScrollOffset,
            duration,
        )
        target.invalidate()
    }

    // We don't need clamp distances (OverScroller encapsulates this behavior)
    private fun fling(
        fromHorizontalScrollOffset: Int = horizontalScrollOffset,
        fromVerticalScrollOffset: Int = verticalScrollOffset,
        horizontalVelocity: Int,
        verticalVelocity: Int,
        minHorizontalScrollOffset: Int = 0,
        minVerticalScrollOffset: Int = 0,
        maxHorizontalScrollOffset: Int = target.computeHorizontalScrollRange(),
        maxVerticalScrollOffset: Int = target.computeVerticalScrollRange(),
        overFlingHorizontalRange: Int = overScrollDistance,
        overFlingVerticalRange: Int = overScrollDistance,
    ) {
        scroller.fling(
            fromHorizontalScrollOffset,
            fromVerticalScrollOffset,
            horizontalVelocity,
            verticalVelocity,
            minHorizontalScrollOffset,
            maxHorizontalScrollOffset,
            minVerticalScrollOffset,
            maxVerticalScrollOffset,
            overFlingHorizontalRange,
            overFlingVerticalRange,
        )
        target.postInvalidateOnAnimation()
    }

    fun scrollPage() {}

    fun scrollLine() {}

    fun onAttach() {
        velocity = VelocityTracker.obtain()
    }

    fun onDetach() {
        velocity.recycle()
        velocity = null
    }

    fun clear() = velocity.clear()

    private fun isScrollAction(
        horizontalDistance: Int,
        verticalDistance: Int,
    ) = isScrollAction(verticalDistance) || isScrollAction(horizontalDistance)

    private fun isScrollAction(
        horizontalDistance: Float,
        verticalDistance: Float,
    ) = isScrollAction(verticalDistance) || isScrollAction(horizontalDistance)

    private fun isScrollAction(value: Int): Boolean = abs(value) > touchSlop

    private fun isScrollAction(value: Float): Boolean = abs(value) > touchSlop

    private fun clampHorizontalScrollOffset(
        offset: Int,
        max: Int = target.computeHorizontalScrollRange() + overScrollDistance,
        min: Int = -overScrollDistance,
    ) = clamp<Int>(
        value = offset,
        max = max,
        min = min,
    )

    private fun clampVerticalScrollOffset(
        offset: Int,
        max: Int = target.computeVerticalScrollRange() + overScrollDistance,
        min: Int = -overScrollDistance,
    ) = clamp<Int>(
        value = offset,
        max = max,
        min = min,
    )

    private inline fun <reified T> clampIfCantScrollHorizontally(
        value: T,
        default: T,
    ): T where T : Number, T : Comparable<T> {
        if (target.canScrollHorizontally) return value
        return default
    }

    private inline fun <reified T> clampIfCantScrollVertically(
        value: T,
        default: T,
    ): T where T : Number, T : Comparable<T> {
        if (target.canScrollVertically) return value
        return default
    }

    private inline fun <reified T> clamp(
        value: T,
        max: T,
        min: T,
    ): T where T : Number, T : Comparable<T> {
        if (value > max) return max
        if (value < min) return min
        return value
    }

    private companion object {
        const val DEFAULT_SMOOTH_SCROLL_DURATION = 200

        private const val SCROLL_STATE_IDLE = 0
        private const val SCROLL_STATE_DRAGGING = 1
        private const val SCROLL_STATE_SETTLING = 2

        private const val NO_POINTER = -1
        private const val VELOCITY_UNITS_PER_SECOND = 1000
        private const val PRIMARY_POINTER_INDEX = 0
    }
}
